/**
 *    Copyright (c) 2011-2014, OpenIoT
 *   
 *    This file is part of OpenIoT.
 *
 *    OpenIoT is free software: you can redistribute it and/or modify
 *    it under the terms of the GNU Lesser General Public License as published by
 *    the Free Software Foundation, version 3 of the License.
 *
 *    OpenIoT is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Lesser General Public License for more details.
 *
 *    You should have received a copy of the GNU Lesser General Public License
 *    along with OpenIoT.  If not, see <http://www.gnu.org/licenses/>.
 *
 *     Contact: OpenIoT mailto: info@openiot.eu
 * @author parobert
 * @author cl3m
 * @author Jerome Rousselot
 * @author gsn_devs
 * @author Mehdi Riahi
 * @author Ali Salehi
 * @author Behnaz Bostanipour
 * @author Timotee Maret
 * @author Julien Eberle
 */

package org.openiot.gsn;

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.GraphicsEnvironment;
import java.awt.RenderingHints;
import java.awt.SplashScreen;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.util.HashMap;
import java.util.Properties;
import java.util.Random;

import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;
import org.eclipse.jetty.http.security.Constraint;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.security.HashLoginService;
import org.eclipse.jetty.security.authentication.BasicAuthenticator;
import org.eclipse.jetty.server.AbstractConnector;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
import org.eclipse.jetty.server.session.HashSessionIdManager;
import org.eclipse.jetty.server.ssl.SslSocketConnector;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.webapp.WebAppContext;
import org.jibx.runtime.BindingDirectory;
import org.jibx.runtime.IBindingFactory;
import org.jibx.runtime.IUnmarshallingContext;
import org.jibx.runtime.JiBXException;
import org.openiot.gsn.beans.ContainerConfig;
import org.openiot.gsn.beans.StorageConfig;
import org.openiot.gsn.beans.VSensorConfig;
import org.openiot.gsn.dynamicSensorControl.DynamicControlTaskTimer;
import org.openiot.gsn.http.ac.ConnectToDB;
import org.openiot.gsn.http.rest.LocalDeliveryWrapper;
import org.openiot.gsn.http.rest.PushDelivery;
import org.openiot.gsn.http.rest.RestDelivery;
import org.openiot.gsn.metadata.LSM.LSMRepository;
import org.openiot.gsn.storage.DBConnectionInfo;
import org.openiot.gsn.storage.SQLValidator;
import org.openiot.gsn.storage.StorageManager;
import org.openiot.gsn.storage.StorageManagerFactory;
import org.openiot.gsn.utils.PropertiesReader;
import org.openiot.gsn.utils.ValidityTools;
import org.openiot.gsn.vsensor.SQLValidatorIntegration;
import org.openiot.gsn.wrappers.WrappersUtil;

import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;

/**
 * Web Service URLs : Microsoft SensorMap:
 * http://localhost:22001/services/Service?wsdl GSN:
 * http://localhost:22001/services/GSNWebService?wsdl
 */
public final class Main {

	private static Main singleton;

	private static int gsnControllerPort;

	private Main() throws Exception {

		ValidityTools.checkAccessibilityOfFiles(DEFAULT_GSN_LOG4J_PROPERTIES,
				WrappersUtil.DEFAULT_WRAPPER_PROPERTIES_FILE,
				DEFAULT_GSN_CONF_FILE);
		ValidityTools
				.checkAccessibilityOfDirs(DEFAULT_VIRTUAL_SENSOR_DIRECTORY);
		PropertyConfigurator.configure(Main.DEFAULT_GSN_LOG4J_PROPERTIES);

		// initializeConfiguration();

		try {
			controlSocket = new GSNController(null, gsnControllerPort);
			containerConfig = loadContainerConfiguration();
			updateSplashIfNeeded(new String[] {
					"GSN is starting at port:"
							+ containerConfig.getContainerPort(),
					"All GSN logs are available at: logs/gsn.log" });
			System.out
					.println("Global Sensor Networks (GSN) is Starting on port "
							+ containerConfig.getContainerPort() + "...");
			System.out
					.println("The logs of GSN server are available in logs/gsn.log file.");
			System.out.println("To Stop GSN execute the gsn-stop script.");
		} catch (FileNotFoundException e) {
			logger.error(new StringBuilder()
					.append("The the configuration file : conf/gsn.xml")
					.append(" doesn't exist.").toString());
			logger.error(e.getMessage());
			logger.error("Check the path of the configuration file and try again.");
			if (logger.isDebugEnabled())
				logger.debug(e.getMessage(), e);
			throw new Exception(e);
		}
		int maxDBConnections = System.getProperty("maxDBConnections") == null ? DEFAULT_MAX_DB_CONNECTIONS
				: Integer.parseInt(System.getProperty("maxDBConnections"));
		int maxSlidingDBConnections = System
				.getProperty("maxSlidingDBConnections") == null ? DEFAULT_MAX_DB_CONNECTIONS
				: Integer.parseInt(System
						.getProperty("maxSlidingDBConnections"));
		int maxServlets = System.getProperty("maxServlets") == null ? DEFAULT_JETTY_SERVLETS
				: Integer.parseInt(System.getProperty("maxServlets"));

		// Init the AC db connection.
		if (Main.getContainerConfig().isAcEnabled() == true) {
			ConnectToDB.init(containerConfig.getStorage().getJdbcDriver(),
					containerConfig.getStorage().getJdbcUsername(),
					containerConfig.getStorage().getJdbcPassword(),
					containerConfig.getStorage().getJdbcURL());
		}

		mainStorage = StorageManagerFactory.getInstance(containerConfig
				.getStorage().getJdbcDriver(), containerConfig.getStorage()
				.getJdbcUsername(), containerConfig.getStorage()
				.getJdbcPassword(), containerConfig.getStorage().getJdbcURL(),
				maxDBConnections);
		//
		StorageConfig sc = containerConfig.getSliding() != null ? containerConfig
				.getSliding().getStorage() : containerConfig.getStorage();
		windowStorage = StorageManagerFactory.getInstance(sc.getJdbcDriver(),
				sc.getJdbcUsername(), sc.getJdbcPassword(), sc.getJdbcURL(),
				maxSlidingDBConnections);
		//
		validationStorage = StorageManagerFactory.getInstance("org.h2.Driver",
				"sa", "", "jdbc:h2:mem:validator",
				Main.DEFAULT_MAX_DB_CONNECTIONS);

		if (logger.isInfoEnabled())
			logger.info("The Container Configuration file loaded successfully.");

		try {
			logger.debug("Starting the http-server @ port: "
					+ containerConfig.getContainerPort()
					+ " (maxDBConnections: " + maxDBConnections
					+ ", maxSlidingDBConnections: " + maxSlidingDBConnections
					+ ", maxServlets:" + maxServlets + ")" + " ...");
			Server jettyServer = getJettyServer(getContainerConfig()
					.getContainerPort(), getContainerConfig().getSSLPort(),
					maxServlets);
			jettyServer.start();
			logger.debug("http-server running @ port: "
					+ containerConfig.getContainerPort());
		} catch (Exception e) {
			throw new Exception(
					"Start of the HTTP server failed. The HTTP protocol is used in most of the communications: "
							+ e.getMessage(), e);
		}
		VSensorLoader vsloader = VSensorLoader
				.getInstance(DEFAULT_VIRTUAL_SENSOR_DIRECTORY);
		controlSocket.setLoader(vsloader);

		String msrIntegration = "gsn.msr.sensormap.SensorMapIntegration";
		try {
			vsloader.addVSensorStateChangeListener((VSensorStateChangeListener) Class
					.forName(msrIntegration).newInstance());
		} catch (Exception e) {
			logger.warn("MSR Sensor Map integration is disabled.");
		}

		vsloader.addVSensorStateChangeListener(new SQLValidatorIntegration(
				SQLValidator.getInstance()));
		vsloader.addVSensorStateChangeListener(DataDistributer
				.getInstance(LocalDeliveryWrapper.class));
		vsloader.addVSensorStateChangeListener(DataDistributer
				.getInstance(PushDelivery.class));
		vsloader.addVSensorStateChangeListener(DataDistributer
				.getInstance(RestDelivery.class));

		ContainerImpl.getInstance().addVSensorDataListener(
				DataDistributer.getInstance(LocalDeliveryWrapper.class));
		ContainerImpl.getInstance().addVSensorDataListener(
				DataDistributer.getInstance(PushDelivery.class));
		ContainerImpl.getInstance().addVSensorDataListener(
				DataDistributer.getInstance(RestDelivery.class));
		vsloader.startLoading();

	}

	private static void closeSplashIfneeded() {
		if (isHeadless())
			return;
		SplashScreen splash = SplashScreen.getSplashScreen();
		// Check if we have specified any splash screen
		if (splash == null) {
			return;
		}
		if (splash.isVisible())
			splash.close();
	}

	private static void updateSplashIfNeeded(String message[]) {
		boolean headless_check = isHeadless();
		for (int i = 0; i < message.length; i++)
			System.out.println(message[i]);

		if (!headless_check) {
			SplashScreen splash = SplashScreen.getSplashScreen();
			if (splash == null)
				return;
			if (splash.isVisible()) {
				// Get a graphics overlay for the splash screen
				Graphics2D g = splash.createGraphics();
				// Do some drawing on the graphics object
				// Now update to the splash screen

				g.setComposite(AlphaComposite.Clear);
				g.fillRect(0, 0, 400, 70);
				g.setPaintMode();
				g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
						RenderingHints.VALUE_ANTIALIAS_ON);
				g.setColor(Color.BLACK);
				g.setFont(new Font("Arial", Font.BOLD, 11));
				for (int i = 0; i < message.length; i++)
					g.drawString(message[i], 13, 16 * i + 10);
				splash.update();
			}
		}
	}

	private static boolean isHeadless() {
		GraphicsEnvironment ge = GraphicsEnvironment
				.getLocalGraphicsEnvironment();
		boolean headless_check = ge.isHeadless();
		return headless_check;
	}

	public synchronized static Main getInstance() {
		if (singleton == null)
			try {
				singleton = new Main();
			} catch (Exception e) {
				logger.error(e.getMessage(), e);
				throw new RuntimeException(e);
			}
		return singleton;
	}

	private static StorageManager mainStorage = null;

	private static StorageManager windowStorage = null;

	private static StorageManager validationStorage = null;

	private GSNController controlSocket;

	private static final int DEFAULT_JETTY_SERVLETS = 100;

	public static final int DEFAULT_MAX_DB_CONNECTIONS = 8;

	public static final String DEFAULT_GSN_LOG4J_PROPERTIES = "conf/log4j.properties";

	public static transient Logger logger = Logger.getLogger(Main.class);

	public static final String DEFAULT_GSN_CONF_FILE = "conf/gsn.xml";

	public static String DEFAULT_VIRTUAL_SENSOR_DIRECTORY = "virtual-sensors";

	public static final String DEFAULT_WEB_APP_PATH = "webapp";

	public static void main(String[] args) {
		Main.gsnControllerPort = Integer.parseInt(args[0]);
		updateSplashIfNeeded(new String[] { "GSN is trying to start.",
				"All GSN logs are available at: logs/gsn.log" });
		try {
			Main.getInstance();
			// checks to see if dynamic sensor control is active
			// and starts timer accordingly
			Config conf=ConfigFactory.load();
			boolean isTrueDynamicControl = conf.getBoolean("dynamicControl");
			if (isTrueDynamicControl)
				DynamicControlTaskTimer.getInstance().startTimer();

		} catch (Exception e) {
			updateSplashIfNeeded(new String[] { "Starting GSN failed! Look at logs/gsn.log for more information." });
			try {
				Thread.sleep(4000);
			} catch (InterruptedException e1) {
				e1.printStackTrace();
			}
		}
		closeSplashIfneeded();
	}

	/**
	 * Mapping between the wrapper name (used in addressing of stream source)
	 * into the class implementing DataSource.
	 */
	private static Properties wrappers;

	private ContainerConfig containerConfig;

	public static ContainerConfig loadContainerConfiguration()
			throws InvalidKeyException, NoSuchAlgorithmException,
			NoSuchProviderException, KeyStoreException, CertificateException,
			SecurityException, SignatureException, IOException {
		ValidityTools.checkAccessibilityOfFiles(
				Main.DEFAULT_GSN_LOG4J_PROPERTIES,
				WrappersUtil.DEFAULT_WRAPPER_PROPERTIES_FILE,
				Main.DEFAULT_GSN_CONF_FILE);
		ValidityTools
				.checkAccessibilityOfDirs(Main.DEFAULT_VIRTUAL_SENSOR_DIRECTORY);
		PropertyConfigurator.configure(Main.DEFAULT_GSN_LOG4J_PROPERTIES);
		ContainerConfig toReturn = null;
		try {
			toReturn = loadContainerConfig(DEFAULT_GSN_CONF_FILE);
			wrappers = WrappersUtil
					.loadWrappers(new HashMap<String, Class<?>>());
			if (logger.isInfoEnabled())
				logger.info(new StringBuilder()
						.append("Loading wrappers.properties at : ")
						.append(WrappersUtil.DEFAULT_WRAPPER_PROPERTIES_FILE)
						.toString());
			if (logger.isInfoEnabled())
				logger.info("Wrappers initialization ...");
		} catch (JiBXException e) {
			logger.error(e.getMessage());
			logger.error(new StringBuilder().append(
					"Can't parse the GSN configuration file : conf/gsn.xml")
					.toString());
			logger.error("Please check the syntax of the file to be sure it is compatible with the requirements.");
			logger.error("You can find a sample configuration file from the GSN release.");
			if (logger.isDebugEnabled())
				logger.debug(e.getMessage(), e);
			System.exit(1);
		} catch (FileNotFoundException e) {
			logger.error(new StringBuilder()
					.append("The the configuration file : conf/gsn.xml")
					.append(" doesn't exist.").toString());
			logger.error(e.getMessage());
			logger.error("Check the path of the configuration file and try again.");
			if (logger.isDebugEnabled())
				logger.debug(e.getMessage(), e);
			System.exit(1);
		} catch (ClassNotFoundException e) {
			logger.error("The file wrapper.properties refers to one or more classes which don't exist in the classpath");
			logger.error(e.getMessage(), e);
			System.exit(1);
		} finally {
			return toReturn;
		}
	}

	/**
	 * This method is called by Rails's Application.rb file.
	 */
	public static ContainerConfig loadContainerConfig(String gsnXMLpath)
			throws JiBXException, FileNotFoundException,
			NoSuchAlgorithmException, NoSuchProviderException, IOException,
			KeyStoreException, CertificateException, SecurityException,
			SignatureException, InvalidKeyException, ClassNotFoundException {
		if (!new File(gsnXMLpath).isFile()) {
			logger.fatal("Couldn't find the gsn.xml file @: "
					+ (new File(gsnXMLpath).getAbsolutePath()));
			System.exit(1);
		}
		IBindingFactory bfact = BindingDirectory
				.getFactory(ContainerConfig.class);
		IUnmarshallingContext uctx = bfact.createUnmarshallingContext();
		ContainerConfig conf = (ContainerConfig) uctx.unmarshalDocument(
				new FileInputStream(new File(gsnXMLpath)), null);
		Class.forName(conf.getStorage().getJdbcDriver());
		conf.setContainerConfigurationFileName(gsnXMLpath);
		return conf;
	}

	// FIXME: COPIED_FOR_SAFE_STOAGE
	public static Properties getWrappers() {
		if (singleton == null)
			return WrappersUtil.loadWrappers(new HashMap<String, Class<?>>());
		return singleton.wrappers;
	}

	// FIXME: COPIED_FOR_SAFE_STOAGE
	public static Class<?> getWrapperClass(String id) {
		try {
			String className = getWrappers().getProperty(id);
			if (className == null) {
				logger.error("The requested wrapper: " + id
						+ " doesn't exist in the wrappers.properties file.");
				return null;
			}

			return Class.forName(className);
		} catch (ClassNotFoundException e) {
			logger.error(e.getMessage(), e);
		}
		return null;
	}

	/**
	 * Get's the GSN configuration without starting GSN.
	 * 
	 * @return
	 * @throws Exception
	 */
	public static ContainerConfig getContainerConfig() {
		if (singleton == null)
			try {
				return loadContainerConfig(DEFAULT_GSN_CONF_FILE);
			} catch (Exception e) {
				return null;
			}
		else
			return singleton.containerConfig;
	}

	public Server getJettyServer(int port, int sslPort, int maxThreads)
			throws IOException {

		Server server = new Server();
		HandlerCollection handlers = new HandlerCollection();
		ContextHandlerCollection contexts = new ContextHandlerCollection();
		server.setThreadPool(new QueuedThreadPool(maxThreads));

		SslSocketConnector sslSocketConnector = null;
		if (sslPort > 0) {
			System.out.println("SSL is Starting on port " + sslPort + "...");
			sslSocketConnector = new SslSocketConnector();
			sslSocketConnector.setPort(getContainerConfig().getSSLPort());
			sslSocketConnector.setKeystore("conf/servertestkeystore");
			sslSocketConnector.setPassword(getContainerConfig()
					.getSSLKeyPassword());
			sslSocketConnector.setKeyPassword(getContainerConfig()
					.getSSLKeyStorePassword());
			sslSocketConnector.setTruststore("conf/servertestkeystore");
			sslSocketConnector.setTrustPassword(getContainerConfig()
					.getSSLKeyStorePassword());
		} else if (getContainerConfig().isAcEnabled())
			logger.error("SSL MUST be configured in the gsn.xml file when Access Control is enabled !");

		// before was connector new SocketConnector();
		// using basic connector for windows
		// bug; Fast option=>SelectChannelConnector
		AbstractConnector connector = new SelectChannelConnector();
		connector.setPort(port);
		connector.setMaxIdleTime(30000);
		connector.setAcceptors(2);
		connector.setConfidentialPort(sslPort);

		if (sslSocketConnector == null)
			server.setConnectors(new Connector[] { connector });
		else
			server.setConnectors(new Connector[] { connector,
					sslSocketConnector });

		WebAppContext webAppContext = new WebAppContext(contexts,
				DEFAULT_WEB_APP_PATH, "/");

		handlers.setHandlers(new Handler[] { contexts, new DefaultHandler() });
		server.setHandler(handlers);

		Properties usernames = new Properties();
		usernames.load(new FileReader("conf/realm.properties"));
		if (!usernames.isEmpty()) {
			HashLoginService loginService = new HashLoginService();
			loginService.setName("GSNRealm");
			loginService.setConfig("conf/realm.properties");
			loginService.setRefreshInterval(10000); // re-reads the file every
													// 10 seconds.

			Constraint constraint = new Constraint();
			constraint.setName("GSN User");
			constraint.setRoles(new String[] { "gsnuser" });
			constraint.setAuthenticate(true);

			ConstraintMapping cm = new ConstraintMapping();
			cm.setConstraint(constraint);
			cm.setPathSpec("/*");
			cm.setMethod("GET");

			ConstraintMapping cm2 = new ConstraintMapping();
			cm2.setConstraint(constraint);
			cm2.setPathSpec("/*");
			cm2.setMethod("POST");

			ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
			securityHandler.setLoginService(loginService);
			securityHandler.setConstraintMappings(new ConstraintMapping[] { cm,
					cm2 });
			securityHandler.setAuthenticator(new BasicAuthenticator());
			webAppContext.setSecurityHandler(securityHandler);
		}

		server.setSendServerVersion(true);
		server.setStopAtShutdown(true);
		server.setSendServerVersion(false);
		server.setSessionIdManager(new HashSessionIdManager(new Random()));

		return server;
	}

	public static StorageManager getValidationStorage() {
		return validationStorage;
	}

	private static HashMap<Integer, StorageManager> storages = new HashMap<Integer, StorageManager>();
	private static HashMap<VSensorConfig, StorageManager> storagesConfigs = new HashMap<VSensorConfig, StorageManager>();

	public static StorageManager getStorage(VSensorConfig config) {
		// StringBuilder sb = new
		// StringBuilder("get storage for: ").append(config == null ? null :
		// config.getName()).append(" -> use ");
		StorageManager sm = storagesConfigs.get(config == null ? null : config);
		if (sm != null)
			return sm;

		DBConnectionInfo dci = null;
		if (config == null || config.getStorage() == null
				|| !config.getStorage().isDefined()) {
			// Use the default storage
			// sb.append("(default) config: ").append(config);
			sm = mainStorage;
		} else {
			// sb.append("(specific) ");
			// Use the virtual sensor specific storage
			if (config.getStorage().isIdentifierDefined()) {
				// TODO get the connection info with the identifier.
				throw new IllegalArgumentException(
						"Identifiers for storage is not supported yet.");
			} else {
				dci = new DBConnectionInfo(config.getStorage().getJdbcDriver(),
						config.getStorage().getJdbcURL(), config.getStorage()
								.getJdbcUsername(), config.getStorage()
								.getJdbcPassword());
			}
			sm = storages.get(dci.hashCode());
			if (sm == null) {
				sm = StorageManagerFactory.getInstance(config.getStorage()
						.getJdbcDriver(),
						config.getStorage().getJdbcUsername(), config
								.getStorage().getJdbcPassword(), config
								.getStorage().getJdbcURL(),
						DEFAULT_MAX_DB_CONNECTIONS);
				storages.put(dci.hashCode(), sm);
				storagesConfigs.put(config, sm);
			}
		}
		// sb.append("storage: ").append(sm.toString()).append(" dci: ").append(dci).append(" code: ").append(dci
		// == null ? null : dci.hashCode());
		// if (logger.isDebugEnabled())
		// logger.warn(sb.toString());
		return sm;

	}

	public static StorageManager getStorage(String vsName) {
		return getStorage(Mappings.getVSensorConfig(vsName));
	}

	public static StorageManager getDefaultStorage() {
		return getStorage((VSensorConfig) null);
	}

	public static StorageManager getWindowStorage() {
		return windowStorage;
	}
	
	
}
